Union Types in GoloScript
This comprehensive guide explains how to use union types in GoloScript: definition, syntax, common patterns, and augmentation.
Introduction
Union types are a powerful GoloScript feature that allows defining a type that can have multiple forms (variants). Itβs similar to TypeScript union types or Rust enums, but with GoloScriptβs dynamic flexibility.
Why Use Unions?
- Type Safety: Explicitly model the possible states of a value
- Expressiveness: Make your code self-documenting
- Pattern Matching: Combine with
matchfor elegant code - Data Structures: Perfect for trees, lists, and other recursive structures
Basic Syntax
Defining a Union
union Result = {
Success = { value }
Failure = { error }
}
This definition creates:
- A
Resulttype with two variants:SuccessandFailure Successcontains avaluefieldFailurecontains anerrorfield
Variants Without Fields
union Status = {
Pending # No fields
Running = { pid }
Done = { exitCode }
}
Three Constructor Syntaxes
GoloScript offers three different syntaxes for creating union instances. Theyβre all equivalent - choose the style that fits your code!
1. Dot Notation (Original Golo Syntax)
let result = Result.Success(42)
let status = Status.Pending()
Advantages: Clear, familiar for Java/JavaScript users
2. Underscore Notation
let result = Result_Failure("error message")
let status = Status_Running(1234)
Advantages: More concise, avoids conflicts with method calls
3. Method Call Notation
let result = Result: Success(100)
let status = Status: Done(0)
Advantages: Consistent with GoloScript method call syntax
Complete Example
# All these syntaxes are valid and equivalent:
let r1 = Result.Success(42)
let r2 = Result_Success(42)
let r3 = Result:Success(42)
# r1, r2, and r3 are identical!
Common Patterns
Pattern 1: Option Type
The Option type represents a value that may or may not exist. Itβs a safer alternative to null.
union Option = { Some = { value }, None }
function safeDivide = |a, b| {
if b == 0 {
return Option_None()
} else {
return Option_Some(a / b)
}
}
let result = safeDivide(10, 2)
if result: isSome() {
println("Result: " + str(result: value()))
}
Use Cases: Operations that can fail, lookups, parsing
Pattern 2: Result Type
The Result type represents either success or failure with an error message.
union Result = { Ok = { value }, Err = { message } }
function validateAge = |age| {
if age < 0 {
return Result_Err("Age cannot be negative")
} else if age > 150 {
return Result_Err("Age too high")
} else {
return Result_Ok(age)
}
}
let validation = validateAge(25)
if validation: isOk() {
println("Valid age: " + str(validation: value()))
} else {
println("Error: " + validation: message())
}
Use Cases: Validation, I/O operations, error handling
Pattern 3: Tree Structure
Unions are perfect for recursive data structures.
union Tree = { Empty, Node = { value, left, right } }
let tree = Tree_Node(
10,
Tree_Node(5, Tree_Empty(), Tree_Empty()),
Tree_Node(15, Tree_Empty(), Tree_Empty())
)
function treeSize = |t| {
if t: isEmpty() {
return 0
} else {
let left = t: left()
let right = t: right()
return 1 + treeSize(left) + treeSize(right)
}
}
println("Tree size: " + str(treeSize(tree))) # 3
Use Cases: Binary trees, lists, AST expressions
Pattern 4: State Machine
Model the states and transitions of a state machine.
union TaskState = {
Pending
Running = { progress }
Completed = { result }
Failed = { error }
}
function describeTask = |state| {
return match {
when state: isPending() then "β³ Pending"
when state: isRunning() then "π Running (" + str(state: progress()) + "%)"
when state: isCompleted() then "β
Completed: " + state: result()
when state: isFailed() then "β Failed: " + state: error()
otherwise "Unknown state"
}
}
Use Cases: Workflows, async processes, UI states
Union Augmentation
Augmentation allows adding methods to union types, creating powerful object-oriented abstractions.
Basic Augmentation
union Option = { Some = { value }, None }
augment Option {
function getOrDefault = |this, defaultValue| {
if this: isSome() {
return this: value()
} else {
return defaultValue
}
}
function isDefined = |this| {
return this: isSome()
}
}
let some = Option_Some(42)
let none = Option_None()
println(some: getOrDefault(0)) # 42
println(none: getOrDefault(0)) # 0
Functional Methods
union Result = { Ok = { value }, Err = { message } }
augment Result {
function map = |this, fn| {
if this: isOk() {
let val = this: value()
return Result_Ok(fn(val))
} else {
return this
}
}
function getOrDefault = |this, defaultValue| {
if this: isOk() {
return this: value()
} else {
return defaultValue
}
}
}
# Chaining operations
let result = Result_Ok(10)
: map(|x| { return x * 2 })
: map(|x| { return x + 10 })
println(result: getOrDefault(0)) # 30
Tree Augmentation
union Tree = { Empty, Node = { value, left, right } }
augment Tree {
function size = |this| {
if this: isEmpty() {
return 0
} else {
return 1 + this: left(): size() + this: right(): size()
}
}
function contains = |this, val| {
if this: isEmpty() {
return false
} else {
if this: value() == val {
return true
} else {
return this: left(): contains(val) or this: right(): contains(val)
}
}
}
}
let tree = Tree_Node(10,
Tree_Node(5, Tree_Empty(), Tree_Empty()),
Tree_Node(15, Tree_Empty(), Tree_Empty())
)
println(tree: size()) # 3
println(tree: contains(5)) # true
println(tree: contains(20)) # false
Variant-Specific Augmentation
The most advanced feature: augmenting individual variants using Union$Variant syntax.
Concept
union Something = {
Yum = { value }
Yuck = { value }
}
# Augment only the Yum variant
augment Something$Yum {
function so = |this, ifYum, ifYuck| {
return ifYum(this: value())
}
}
# Augment only the Yuck variant
augment Something$Yuck {
function so = |this, ifYum, ifYuck| {
return ifYuck(this: value())
}
}
let banana = Something.Yum("π")
let hotPepper = Something.Yuck("πΆοΈ")
banana: so(
|v| { return "Yum! " + v },
|v| { return "Yuck! " + v }
) # β "Yum! π"
Option with Variant Augmentation
union Option = { Some = { value }, None }
# General method for all variants
augment Option {
function isDefined = |this| {
return this: isSome()
}
}
# Specific method for Some
augment Option$Some {
function getOrElse = |this, default| {
return this: value()
}
function map = |this, fn| {
return Option.Some(fn(this: value()))
}
}
# Specific method for None
augment Option$None {
function getOrElse = |this, default| {
return default
}
function map = |this, fn| {
return this # Does nothing
}
}
Why Use Variant Augmentation?
- Polymorphism: Different behavior per variant
- Cleaner Code: No
ifstatements in methods - Performance: Direct dispatch to the right implementation
- Semantics: Each variant has its own specific methods
Result with Variant Augmentation
union Result = { Ok = { value }, Err = { message } }
# General methods
augment Result {
function isSuccess = |this| {
return this: isOk()
}
}
# Ok-specific methods
augment Result$Ok {
function unwrap = |this| {
return this: value()
}
function mapValue = |this, fn| {
return Result.Ok(fn(this: value()))
}
}
# Err-specific methods
augment Result$Err {
function unwrap = |this| {
println("β οΈ Attempted to unwrap an error!")
return null
}
function mapValue = |this, fn| {
return this # Errors are not transformed
}
}
Complete Practical Examples
Safe Calculator
union CalcResult = { Success = { value }, Error = { message } }
augment CalcResult {
function map = |this, fn| {
if this: isSuccess() {
return CalcResult.Success(fn(this: value()))
} else {
return this
}
}
function display = |this| {
if this: isSuccess() {
return "β
" + str(this: value())
} else {
return "β " + this: message()
}
}
}
function safeDivide = |a, b| {
if b == 0 {
return CalcResult_Error("Division by zero")
} else {
return CalcResult_Success(a / b)
}
}
# Usage
let result = safeDivide(100, 4)
: map(|x| { return x * 2 })
: map(|x| { return x + 10 })
println(result: display()) # β
60
Task State Machine
union TaskState = {
Pending
Running = { progress }
Completed = { result }
Failed = { error }
}
# Each variant has its own transition methods
augment TaskState$Pending {
function start = |this| {
return TaskState.Running(0)
}
}
augment TaskState$Running {
function updateProgress = |this, newProgress| {
if newProgress >= 100 {
return TaskState.Completed("Task completed successfully")
} else {
return TaskState.Running(newProgress)
}
}
function fail = |this, error| {
return TaskState.Failed(error)
}
}
augment TaskState$Completed {
function restart = |this| {
return TaskState.Pending()
}
}
augment TaskState$Failed {
function retry = |this| {
return TaskState.Pending()
}
}
# Lifecycle simulation
var task = TaskState.Pending()
task = task: start() # β Running(0)
task = task: updateProgress(50) # β Running(50)
task = task: updateProgress(100) # β Completed
task = task: restart() # β Pending
Best Practices
β Do
-
Use descriptive variant names
# β Clear union HttpResponse = { Success = { data }, Error = { message } } # β Ambiguous union Response = { A = { x }, B = { y } } -
Combine with match for elegant code
function handleResponse = |response| { return match { when response: isSuccess() then processData(response: data()) when response: isError() then logError(response: message()) otherwise "Unknown" } } -
Use auto-generated methods
# GoloScript automatically generates: isSuccess(), isError() if result: isSuccess() { # Processing... } -
Augment with utility methods
augment Result { function isFailure = |this| { return this: isErr() } function getOrDefault = |this, default| { if this: isOk() { return this: value() } else { return default } } }
β Donβt
-
Too many variants
# β Hard to maintain union Status = { Status1, Status2, Status3, Status4, Status5, Status6, Status7, Status8 } # β Group logically union TaskStatus = { Pending, Running = { progress }, Done = { result } } union ErrorStatus = { NetworkError = { code }, ValidationError = { field } } -
Variants with too many fields
# β Too complex union User = { Active = { id, name, email, age, address, phone, role, dept } } # β Use structs struct UserData = { id, name, email, age, address, phone, role, dept } union UserStatus = { Active = { data }, Inactive = { reason } } -
Ignore errors
# β Ignores errors let result = operation() println(result: value()) # Crash if it's an error! # β Always check if result: isOk() { println(result: value()) } else { println("Error: " + result: message()) }
Comparison with Other Languages
| Feature | GoloScript | Rust | TypeScript | Haskell |
|---|---|---|---|---|
| Union types | β
union Result = { Ok, Err } |
β
enum Result<T,E> |
β
type Result = Ok | Err |
β
data Result = Ok | Err |
| Pattern matching | β
match + isXxx() |
β
match |
β οΈ Type guards | β
case |
| Augmentation | β
augment Union |
β
impl |
β οΈ Prototype/class | β Instance |
| Variant aug. | β
augment Union$Variant |
β (via traits) | β | β |
| Syntax | 3 syntaxes! | 1 syntax | 1 syntax | 1 syntax |
Summary
Union types in GoloScript are:
- Expressive: Clearly model possible states
- Flexible: 3 construction syntaxes to choose from
- Powerful: General and variant-specific augmentation
- Practical: Common patterns for Option, Result, Tree, State Machine
General Recommendation: Start with simple patterns (Option, Result), then explore augmentation for richer abstractions. Variant-specific augmentation is reserved for advanced cases requiring sophisticated polymorphism.
Β© 2026 GoloScript Project | Built with Gu10berg
Subscribe: π‘ RSS | βοΈ Atom